1 module targets.android;
2 import commons;
3 import std.net.curl;
4 import std.path;
5 
6 ///This is the one which will be installed when using the SDK.
7 enum TargetAndroidSDK = 31;
8 enum TargetAndroidNDK = "21.4.7075529";
9 enum Ldc2AndroidAarchLibReleaseLink = "https://github.com/MrcSnm/HipremeEngine/releases/download/BuildAssets.v1.0.0/android.zip";
10 enum CurrentlySupportedLdc2Version = "ldc2 1.33.0-beta1";
11 ///Use a random Adb Port 
12 enum HipremeEngineAdbPort = "55565";
13 
14 enum FindAndroidNdkResult
15 {
16 	NotFound,
17 	Found,
18 	MustInstallSdk,
19 	MustInstallNdk
20 }
21 
22 private FindAndroidNdkResult tryFindAndroidNDK(ref Terminal t, ref RealTimeConsoleInput input)
23 {
24 	if("ANDROID_NDK_HOME" in environment)
25 	{
26 		configs["androidNdkPath"] = environment["ANDROID_NDK_HOME"];
27 		string sdk = getFirstExistingVar("ANDROID_SDK", "ANDROID_SDK_HOME");
28 		if(!sdk.length)
29 			sdk = buildNormalizedPath(configs["androidNdkPath"].str, "..", "..");
30 		configs["androidSdkPath"] = sdk;
31 		return FindAndroidNdkResult.Found;
32 	}
33 	bool isValidNDK(string chosenNDK)
34 	{
35 		import std.conv:to;
36 		int ndkVer = chosenNDK[0..2].to!int;
37 		return ndkVer <= 21;
38 	}
39 	version(Windows)
40 	{
41 		string locAppData = environment["LOCALAPPDATA"];
42 		if(locAppData == null)
43 		{
44 			t.writelnError("Could not find %LOCALAPPDATA% in your Windows.");
45 			t.flush;
46 			return FindAndroidNdkResult.NotFound;
47 		}
48 		string sdkPath = buildNormalizedPath(locAppData, "Android", "Sdk");
49 		string tempNdkPath = sdkPath;
50 
51 		if(!std.file.exists(sdkPath))
52 		{
53 			t.writelnError("Could not find ", sdkPath, ". You need to install Android SDK.");
54 			t.flush;
55 			return FindAndroidNdkResult.MustInstallSdk;
56 		}
57 		tempNdkPath = buildNormalizedPath(sdkPath, "ndk");
58 		if(!std.file.exists(tempNdkPath))
59 		{
60 			t.writelnError("Could not find ", tempNdkPath, ". You need to have at least one NDK installed.");
61 			t.flush;
62 			return FindAndroidNdkResult.MustInstallNdk;
63 		}
64 		do
65 		{
66 			string ndkPath = selectInFolder(
67 				"Select the NDK which you want to use. Remember that only NDK <= 21 is supported.", 
68 				tempNdkPath, t, input
69 			);
70 			if(isValidNDK(ndkPath))
71 			{
72 				tempNdkPath = ndkPath;
73 				break;
74 			}
75 			t.writelnError("Please select a valid NDK (<= 21)");
76 		} while(true);
77 		t.writelnSuccess("Chosen "~ tempNdkPath~ " as your NDK.");
78 		environment["androidNdkPath"] = sdkPath;
79 		environment["androidSdkPath"] = tempNdkPath;
80 		return FindAndroidNdkResult.Found;
81 	}
82 	else version(linux)
83 	{
84 		return FindAndroidNdkResult.NotFound;
85 	}
86 	else version(OSX)
87 	{
88 		return FindAndroidNdkResult.NotFound;
89 	}
90 
91 }
92 
93 private string getAndroidSDKDownloadLink()
94 {
95 	version(Windows) return "https://dl.google.com/android/repository/commandlinetools-win-9477386_latest.zip";
96 	else version(linux) return "https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip";
97 	else version(OSX) return "https://dl.google.com/android/repository/commandlinetools-mac-9477386_latest.zip";
98 	else assert(false, "Your system does not have an Android SDK.");
99 }
100 
101 private string getOpenJDKDownloadLink()
102 {
103 	version(Windows) return "https://aka.ms/download-jdk/microsoft-jdk-11.0.18-windows-x64.zip";
104 	else version(linux) return "https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz";
105 	else version(OSX) return "https://download.java.net/java/GA/jdk11/13/GPL/openjdk-11.0.1_osx-x64_bin.tar.gz";
106 	else assert(false, "Your system does not have an OpenJDK");
107 }
108 
109 private string getAndroidFlagsToolchains()
110 {
111 	version(Windows)
112 	{
113 		import std.string:replace;
114 		return "-gcc=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang.cmd").replace("\\", "/") ~"\" " ~
115 		"-linker=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android-ld.bfd.exe").replace("\\", "/") ~"\" " ~
116 		///Put the lib path for finding libandroid, liblog, libOpenSLES, libEGL and libGLESv3
117 		"-L-L\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/aarch64-linux-android/30/").replace("\\", "/")~"\" "
118 		;
119 	}
120 	else version(linux)
121 	{
122 		return "-gcc=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang") ~"\" " ~
123 		"-linker=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-ld.bfd") ~"\" " ~
124 		///Put the lib path for finding libandroid, liblog, libOpenSLES, libEGL and libGLESv3
125 		"-L-L\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/30/")~"\" "
126 		;
127 	}
128 	else version(OSX)
129 	{
130 		return "-gcc=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang") ~"\" " ~
131 		"-linker=\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ld.bfd") ~"\" " ~
132 		///Put the lib path for finding libandroid, liblog, libOpenSLES, libEGL and libGLESv3
133 		"-L-L\""~buildNormalizedPath(configs["androidNdkPath"].str, "toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/30/")~"\" "
134 		;
135 	}
136 }
137 
138 private string getPackagesToInstall()
139 {
140 	import std.conv:to;
141 	string packages = `"build-tools;`~to!string(TargetAndroidSDK)~`.0.0" `~ 
142 		`"extras;google;webdriver" ` ~
143 		`"platform-tools" ` ~
144 		`"ndk;`~TargetAndroidNDK~`" `~
145 		`"platforms;android-`~to!string(TargetAndroidSDK)~`" `~
146 		`"sources;android-`~to!string(TargetAndroidSDK)~`" `;
147 
148 	version(Windows)
149 	{
150 		packages~= `"extras;intel;Hardware_Accelerated_Execution_Manager" `~
151 					`"extras;google;usb_driver" `;
152 	}
153 	return packages;
154 }
155 
156 
157 private bool downloadOpenJDK(ref Terminal t, ref RealTimeConsoleInput input)
158 {
159 	string javaContainer = "openjdk_11.zip";
160 	version(Posix) javaContainer = javaContainer.setExtension(".tar.gz");
161 	javaContainer = buildNormalizedPath(std.file.tempDir, javaContainer);
162 	downloadFileIfNotExists("OpenJDK for building to Android. ", getOpenJDKDownloadLink(), javaContainer, t, input);
163 
164 	string outputPath = buildNormalizedPath(std.file.getcwd(), "Android", "openjdk_11");
165 	return extractToFolder(javaContainer, outputPath, t, input);
166 }
167 
168 private bool downloadAndroidSDK(ref Terminal t, ref RealTimeConsoleInput input, out string sdkPath)
169 {
170 	import std.file:tempDir;
171 	import std.zip;
172 
173 	string androidSdkZip = buildNormalizedPath(tempDir, "android_sdk.zip");
174 
175 	if(!downloadFileIfNotExists("Android SDK will be installed on your system, do you accept it?", 
176 		getAndroidSDKDownloadLink(), androidSdkZip, t, input))
177 		return false;
178 
179 	string outputDirectory = buildNormalizedPath(std.file.getcwd(), "Android", "Sdk");
180 	sdkPath = outputDirectory;
181 	string finalOutput = buildNormalizedPath(outputDirectory, "cmdline-tools", "latest");
182 
183 	if(!std.file.exists(finalOutput))
184 	{
185 		if(!extractZipToFolder(androidSdkZip, outputDirectory, t))
186 			return false;
187 		std.file.rename(buildNormalizedPath(outputDirectory, "cmdline-tools/"), buildNormalizedPath(outputDirectory, "latest/"));
188 		std.file.mkdirRecurse(buildNormalizedPath(outputDirectory, "cmdline-tools"));
189 		std.file.rename(buildNormalizedPath(outputDirectory, "latest"), finalOutput);
190 	}
191 	return true;
192 }
193 
194 private bool downloadAndroidLibraries(ref Terminal t, ref RealTimeConsoleInput input)
195 {
196 	string androidZipDir = buildNormalizedPath(std.file.tempDir(), "androidLibs.zip");
197 	if(!downloadFileIfNotExists("Do you accept downloading android libraries for "~CurrentlySupportedLdc2Version, 
198 		Ldc2AndroidAarchLibReleaseLink, androidZipDir, t, input))
199 		return false;
200 
201 	string outputDir = buildNormalizedPath(std.file.getcwd(), "Android", "ldcLibs");
202 	extractZipToFolder(androidZipDir, outputDir, t);
203 
204 	return true;
205 }
206 
207 
208 private bool installAndroidNDK(ref Terminal t, string sdkPath)
209 {
210 	string finalOutput = buildNormalizedPath(sdkPath, "cmdline-tools", "latest");
211 	t.writeln("Updating SDK manager.");
212 	t.flush;
213 
214 
215 	string sdkManagerPath = buildNormalizedPath(finalOutput, "bin");
216 
217 	if(!makeFileExecutable(buildNormalizedPath(sdkManagerPath, "sdkmanager")))
218 	{
219 		t.writeln("Failed to set sdkmanager as executable.");
220 		t.flush;
221 		return false;
222 	}
223 
224 	string execSdkManager = "sdkmanager ";
225 	version(Posix) execSdkManager = "./sdkmanager";
226 
227 	if(wait(spawnShell("cd "~sdkManagerPath~" && "~execSdkManager~" --install")) != 0)
228 	{
229 		t.writeln("Failed on installing SDK.");
230 		t.flush;
231 		return false;
232 	}
233 
234 	t.writeln("Installing packages: ", getPackagesToInstall());
235 	t.writeln("You will need to accept some permissions, this process may take a little bit of time.");
236 	t.flush;
237 	if(wait(spawnShell("cd "~sdkManagerPath~" && "~execSdkManager ~" " ~getPackagesToInstall())) != 0)
238 	{
239 		t.writeln("Failed on installing NDK.");
240 		t.flush;
241 		return false;
242 	}
243 	if(!makeFileExecutable(buildNormalizedPath(sdkPath, "platform-tools", "adb")))
244 	{
245 		t.writeln("Failed to set adb as executable.");
246 		t.flush;
247 		return false;
248 	}
249 
250 	configs["androidSdkPath"] = sdkPath;
251 	configs["androidNdkPath"] = buildNormalizedPath(sdkPath, "ndk", TargetAndroidNDK);
252 	updateConfigFile();
253 	return true;
254 }
255 
256 private bool installOpenJDK(ref Terminal t, ref RealTimeConsoleInput input)
257 {
258 	if(!("javaHome" in configs))
259 	{
260 		if(!("JAVA_HOME" in environment))
261 		{
262 			t.writelnHighlighted("JAVA_HOME wasn't found in your environment. 
263 				Build Selector will download a compatible OpenJDK for Android Development.");
264 			t.flush;
265 			if(!downloadOpenJDK(t, input))
266 			{
267 				t.writelnError("Could not download OpenJDK");
268 				return false;
269 			}
270 			string javaHome = buildNormalizedPath(std.file.getcwd(), "Android", "openjdk_11");
271 			version(Windows) javaHome = buildNormalizedPath(javaHome, "jdk-11.0.18+10");
272 			else version(linux) javaHome = buildNormalizedPath(javaHome, "jdk-11.0.2");
273 			else version(OSX) javaHome = buildNormalizedPath(javaHome, "jdk-11.0.1.jdk", "Contents", "Home");
274 			else assert(false, "Your OS is not supported.");
275 			if(!std.file.exists(javaHome))
276 			{
277 				t.writelnError("Expected JAVA_HOME at automatic installation does not exists:" ~ javaHome);
278 				t.flush();
279 				return false;
280 			}
281 			configs["javaHome"] = javaHome;
282 			updateConfigFile();
283 		}
284 		else
285 		{
286 			configs["javaHome"] = environment["JAVA_HOME"];
287 			updateConfigFile();
288 		}
289 	}
290 	return true;
291 }
292 
293 private bool installAndroidSDK(ref Terminal t, ref RealTimeConsoleInput input)
294 {
295 	if(!("androidNdkPath" in configs) || !("androidSdkPath" in configs))
296 	{
297 		FindAndroidNdkResult res = tryFindAndroidNDK(t, input);
298 		switch(res)
299 		{
300 			case FindAndroidNdkResult.NotFound:
301 			{
302 				string sdkPath;
303 				if(!downloadAndroidSDK(t, input, sdkPath))
304 				{
305 					t.writelnError("Android SDK download didn't succeed");
306 					return false;
307 				}
308 				if(!installAndroidNDK(t, sdkPath))
309 				{
310 					t.writelnError("Could not install Android NDK.");
311 					return false;
312 				}
313 				break;
314 			}
315 			case FindAndroidNdkResult.Found:
316 				updateConfigFile();
317 				break;
318 			default: assert(false, "Case not yet implemented.");
319 		}
320 	}
321 	return true;
322 }
323 
324 
325 private void runAndroidApplication(ref Terminal t)
326 {
327 	version(Windows)
328 	{
329 		string adb = buildNormalizedPath(configs["androidSdkPath"].str, "platform-tools", "adb.exe");
330 		string gradlew = "gradlew.bat";
331 	}
332 	else version(Posix)
333 	{
334 		string adb = buildNormalizedPath(configs["androidSdkPath"].str, "platform-tools", "adb");
335 		string gradlew = "./gradlew";
336 
337 		if(!makeFileExecutable(buildNormalizedPath("build", "android", "project", "gradlew")))
338 		{
339 			t.writelnError("Could not make gradlew executable.");
340 			return;
341 		}
342 	}
343 
344 	std.file.chdir(buildNormalizedPath("build", "android", "project"));
345 
346 	wait(spawnShell(gradlew ~ " :app:assembleDebug"));
347 
348 	string adbInstall = adb~" install -r "~
349 		buildNormalizedPath(std.file.getcwd(), "app", "build", "outputs", "apk", "debug", "app-debug.apk");
350 
351 	t.writeln("Executing adb install: ", adbInstall);
352 	t.flush;
353 	environment["ANDROID_ADB_SERVER_PORT"] = HipremeEngineAdbPort;
354 	wait(spawnShell(adbInstall));
355 	wait(spawnShell(adb~" shell monkey -p com.hipremeengine.app 1"));
356 }
357 
358 ChoiceResult prepareAndroid(Choice* c, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions cOpts)
359 {
360 	if(!installOpenJDK(t, input))
361 	{
362 		t.writelnError("Failed installing OpenJDK.");
363 		return ChoiceResult.Error;
364 	}
365 	environment["JAVA_HOME"] = configs["javaHome"].str;
366 	if(!installAndroidSDK(t, input))
367 	{
368 		t.writelnError("Failed installing Android SDK.");
369 		return ChoiceResult.Error;
370 	}
371 	
372 
373 	if(!std.file.exists(
374 		buildNormalizedPath(std.file.getcwd(), "Android", "ldcLibs", "android", "lib", "libdruntime-ldc.a")))
375 	{
376 		if(!downloadAndroidLibraries(t, input))
377 		{
378 			t.writelnError("Failed downloading ldc android libraries.");
379 			t.flush;
380 			return ChoiceResult.Error;
381 		}
382 	}
383 	environment["ANDROID_HOME"] = configs["androidSdkPath"].str;
384 
385 	runEngineDScript(t, "releasegame.d", configs["gamePath"].str);
386 	putResourcesIn(t, getHipPath("build", "android", "project", "app", "src", "main", "assets"));
387 	cached(() => timed(() => outputTemplateForTarget(t)));
388 
389 	string ldcLibsPath = buildNormalizedPath(std.file.getcwd(), "Android", "ldcLibs", "android", "lib");
390 
391 
392 	string nextReleaseFlags = "-defaultlib=phobos2-ldc,druntime-ldc " ~
393 		"-link-defaultlib-shared=false " ~
394 		"-L-L\""~ ldcLibsPath ~"\" " ~
395 		"-L-rpath=\""~ ldcLibsPath~"\" ";
396 
397 	environment["DFLAGS"] = nextReleaseFlags ~ getAndroidFlagsToolchains();
398 	t.writeln(environment["DFLAGS"]);
399 	t.flush;
400 
401 	std.file.chdir(configs["hipremeEnginePath"].str);
402 	if(waitDubTarget(t, "android", DubArguments().command("build").arch("aarch64--linux-android").opts(cOpts)) != 0)
403 	{
404 		t.writelnError("Compilation failed.");
405 		return ChoiceResult.Error;
406 	}
407 
408 	std.file.rename(
409 		buildNormalizedPath("bin", "android", "libhipreme_engine.so"),
410 		buildNormalizedPath("build", "android", "project", "app", "src", "main", "jniLibs", "arm64-v8a", "libhipreme_engine.so")
411 	);
412 	runAndroidApplication(t);
413 
414 	return ChoiceResult.Continue;
415 }